<?php
set_magic_quotes_runtime(0);
error_reporting(1);
class XmlParser {
    var $parser = null;
    var $output = array();
	var $options = array();
	var $currentTag;
    var $tagStack;
    protected $_errorMessages = array();
	
	private $__defaultOptions = array(
		'case_folding' => 0,
		'structured' => 1,
		'whitespace' => 0,
		'encoding' => 'UTF-8'
	);

/**
 * Parse XML from any source, detect if it is a path of a file or string of xml
 * @access public
 * @param string $xml a path to xml file or string of xml data
 * @param array $options options
 * @return bool return true on success 
 */    
    function XmlParser($xml = '', $options = null) {
        if (!empty($xml) && is_string($xml)) {
            // is it a file
            if ( is_file($xml) ) {
				$this->__setup($options);
                return $this->__parse($xml);    
            } else { // it is a string of xml
				$this->__setup($options);
                return $this->__parseStream($xml);
            }
        }
        
        return false;
    }

/**
 * Setup parser and options
 * @access private
 * @param array $options options
 */	
	private function __setup($options = null) {
		// set options
		if ( is_array($options) && !empty($options) ) {
			$this->options = array_merge( $this->__defaultOptions, $options );
			
			if ( empty($this->options['encoding']) ) {
				$this->options['encoding'] = $this->__defaultOptions['encoding'];
			}
		} else { // use default options
			$this->options = $this->__defaultOptions;
		}
        if ( $this->options['structured'] ) {
            $this->currentTag =& $this->output;
            $this->tagStack = array();
        }
		
		// prepare parser
        $this->parser = xml_parser_create();
        xml_set_object($this->parser, $this);
		
		if ( !$this->options['whitespace'] ) {
			// XML_OPTION_SKIP_WHITE didn't work on some php version, handled with trim on cdata handler
			xml_parser_set_option($this->parser, XML_OPTION_SKIP_WHITE, 1);
		}
		
		if ( !$this->options['case_folding'] ) {
			// prevent uppercasing on tag's name
			xml_parser_set_option($this->parser, XML_OPTION_CASE_FOLDING, 0);
		}
		
		// set encoding
		xml_parser_set_option($this->parser, XML_OPTION_TARGET_ENCODING, $this->options['encoding']);
		
		// set handler
        xml_set_character_data_handler($this->parser, 'dataHandler');
        xml_set_element_handler($this->parser, 'startHandler', 'endHandler');
	}

/**
 * Parse XML from file
 * @access private
 * @param string $path Path to the XML file
 * @return bool true return true on success
 */
    private function __parse($path) {
        if ( !($fp = fopen($path, "r")) ) {
            //die("Cannot open XML data file: $path");
            $this->_errorMessages[] = "Cannot open XML data file: $path";
            return false;
        }
        
        while ($data = fread($fp, 4096)) {
            if (!xml_parse($this->parser, $data, feof($fp))) {
                //die(sprintf("XML error: %s at line %d", xml_error_string(xml_get_error_code($this->parser)), xml_get_current_line_number($this->parser)));
                $this->_errorMessages[] = "XML error: " . xml_error_string(xml_get_error_code($this->parser)) . " at line " . xml_get_current_line_number($this->parser);
                xml_parser_free($this->parser);
                return false;
            }
        }
        
        return true;
    }
	
/**
 * Parse XML from string
 * @access private
 * @param string $data string of XML
 * @return bool true return true on success
 */    
    private function __parseStream($data) {
        if (!xml_parse( $this->parser, $data, true )) {
            //die(sprintf("XML error: %s at line %d", xml_error_string(xml_get_error_code($this->parser)), xml_get_current_line_number($this->parser)));
            $this->_errorMessages[] = "XML error: " . xml_error_string(xml_get_error_code($this->parser)) . " at line " . xml_get_current_line_number($this->parser);
            xml_parser_free($this->parser);
            return false;
        }
        
        return true;
    }

/**
 * Start handler for opening tag
 * @access public
 */    
    function startHandler($parser, $name, $attributes) {
        $content = array();
		
		if ( !$this->options['structured'] ) {
			$content['name'] = $name;
			
			if ( !empty($attributes) ) {
				$content['attributes'] = $attributes;
			}
		} else {
			if (!isset($this->currentTag[$name])) {
				$this->currentTag[$name] = array();
			}
			
			$tag = array();
			if (!empty($attributes)) {
				$tag['attributes'] = $attributes;
			}
			array_push($this->currentTag[$name], $tag);
			
			$t =& $this->currentTag[$name];
			$this->currentTag =& $t[count($t) - 1];
			array_push($this->tagStack, $name);
            
            return;
		}
        array_push( $this->output, $content );
    }
    
/**
 * character handler inside tag
 * @access public
 */    
    function dataHandler($parser, $data) {
        if ( !empty($data) ) {
            $key = count( $this->output ) - 1;
			if ( !trim($data) && !$this->options['whitespace'] ) {
				return;
			}
			if ( !$this->options['structured'] ) {
                $this->output[$key]['content'] .= $data;
			} else {
				if (isset($this->currentTag['content'])) {
                    $this->currentTag['content'] .= $data; 
                } else {
                    $this->currentTag['content'] = $data;
                }
			}
        }
    }
/**
 * end handler for closing tag
 * @access public
 */    
    function endHandler($parser, $name) {
        if (!$this->options['structured']) {
            if ( count($this->output) > 1 ) {           
                $data = array_pop($this->output);
                $key  = count($this->output) -1;
                
                if ( !$this->output[$key]['child'] ) {
                    $this->output[$key]['child'] = array();
                }
                array_push($this->output[$key]['child'], $data);
            }
        } else {
            $this->currentTag =& $this->output;
            array_pop($this->tagStack);
            
            for ($i = 0; $i < count($this->tagStack); $i++) {
                $t =& $this->currentTag[$this->tagStack[$i]];
                $this->currentTag =& $t[count($t)-1];
            }
        }
    }
    
    function getErrorMessages() {
        return $this->_errorMessages;
    }
}
?>
